/**
 * 
 */
package qy.jalgotrade.optimizer.worker;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.apache.xmlrpc.client.util.ClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import qy.jalgotrade.bar.Bar;
import qy.jalgotrade.bar.Bars;
import qy.jalgotrade.barfeed.BaseBarFeed;
import qy.jalgotrade.barfeed.OptimizerBarFeed;
import qy.jalgotrade.optimizer.Serialization;
import qy.jalgotrade.optimizer.base.OptimizingGoal;
import qy.jalgotrade.optimizer.base.Parameters;
import qy.jalgotrade.optimizer.base.Results;
import qy.jalgotrade.optimizer.xmlrpc.Job;
import qy.jalgotrade.optimizer.xmlrpc.JobManager;

/**
 * @author c-geo
 *
 */
public abstract class AbstractWorker implements Runnable {

	public static final String ENCODING = "UTF-8";

	public static final boolean ENABLE_EXCEPTION = true;

	public static final int CONNECTION_TIME_OUT = 5000;

	public static final int REPLY_TIME_OUT = 60000;

	private static Logger __logger = LoggerFactory.getLogger(AbstractWorker.class);

	private String __workerName;

	private JobManager __jobManPrxy;

	private OptimizingGoal __goal;

	/**
	 * 
	 * @param address
	 * @param port
	 * @throws UnknownHostException
	 * @throws MalformedURLException
	 */
	public AbstractWorker(String address, int port) throws UnknownHostException, MalformedURLException {

		this(address, port, new OptimizingGoal(), "");
	}

	/**
	 * 
	 * @param address
	 * @param port
	 * @param goal
	 * @throws UnknownHostException
	 * @throws MalformedURLException
	 */
	public AbstractWorker(String address, int port, OptimizingGoal goal)
	        throws UnknownHostException, MalformedURLException {

		this(address, port, goal, "");
	}

	/**
	 * 
	 * @param address
	 * @param port
	 * @param goal
	 * @param workerName
	 * @throws UnknownHostException
	 * @throws MalformedURLException
	 */
	public AbstractWorker(String address, int port, OptimizingGoal goal, String workerName)
	        throws UnknownHostException, MalformedURLException {

		if (workerName == null || workerName.length() == 0) {
			__workerName = InetAddress.getLocalHost().getHostName();
		} else {
			__workerName = workerName;
		}

		__goal = goal;

		String url = String.format("http://%s:%s/PyAlgoTradeRPC", address, Integer.valueOf(port));

		XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
		config.setServerURL(new URL(url));
		config.setEnabledForExtensions(true);
		config.setEnabledForExceptions(ENABLE_EXCEPTION);
		config.setContentLengthOptional(false);
		config.setEncoding(ENCODING);
		config.setConnectionTimeout(CONNECTION_TIME_OUT);
		config.setReplyTimeout(REPLY_TIME_OUT);

		XmlRpcClient client = new XmlRpcClient();
		client.setConfig(config);
		ClientFactory factory = new ClientFactory(client);

		__jobManPrxy = (JobManager) factory.newInstance(JobManager.class);
	}

	/**
	 * 
	 * @return
	 */
	public String getWorkerName() {

		return __workerName;
	}

	/**
	 * 
	 */
	public void changeWorkerName(String name) {

		__workerName = name;
	}

	/**
	 * 
	 * @return
	 */
	public OptimizingGoal getGoal() {

		return __goal;
	}

	/**
	 * 
	 * @return
	 */
	public static Logger getLogger() {

		return __logger;
	}

	/**
	 * 
	 * @return
	 */
	public Pair<List<String>, List<Bars>> getInstrumentsAndBars() {

		byte[] serializedObj = __jobManPrxy.getInstrumentsAndBars();
		Pair<List<String>, List<Bars>> ret = Serialization.loads(serializedObj);
		return ret;
	}

	/**
	 * 
	 * @return
	 */
	public Bar.Frequency getBarsFrequency() {

		String freqString = __jobManPrxy.getBarsFrequency();
		return Bar.Frequency.valueOf(freqString);
	}

	/**
	 * 
	 * @return
	 */
	public Job getNextJob() {

		byte[] serializedObj = __jobManPrxy.getNextJob();
		Job ret = Serialization.loads(serializedObj);
		return ret;
	}

	/**
	 * 
	 * @param jobId
	 * @param result
	 * @param bestParams
	 */
	public void pushJobResults(int jobId, List<Results> jobResults, Results bestResult) {

		byte[] serializedJobResults = Serialization.dumps(jobResults);
		byte[] serializedBestResult = Serialization.dumps(bestResult);

		__jobManPrxy.pushJobResults(jobId, serializedJobResults, serializedBestResult, __workerName);
	}

	/**
	 * 
	 * @param job
	 * @param barsFreq
	 * @param instruments
	 * @param bars
	 * @throws Exception
	 */
	private void __processJob(Job job, Bar.Frequency barsFreq, List<String> instruments, List<Bars> bars)
	        throws Exception {

		Results bestResult = null;
		Parameters parameters = job.getNextParameters();
		List<Results> jobResults = new ArrayList<>();
		while (parameters != null) {
			// Wrap the bars into a feed.
			OptimizerBarFeed feed = new OptimizerBarFeed(barsFreq, instruments, bars);
			// Run the strategy.
			getLogger().info("Running strategy with parameters {}", parameters);
			Results result = null;
			try {
				result = runStrategy(feed, parameters);
			} catch (Exception e) {
				getLogger().error("Error running strategy with parameters {}: {} (exception stack trace {})",
				        parameters, e, ExceptionUtils.getStackTrace(e));
			}

			getLogger().info("Result: {}", result);

			if (result != null) {

				jobResults.add(result);

				if (Arrays.asList(Results.KPI.RESULT, Results.KPI.RETURN, Results.KPI.SHARPE, Results.KPI.WIN_RATE)
				        .contains(__goal.getGoalKpi())) {

					if (bestResult == null
					        || result.getBy(__goal.getGoalKpi()) > bestResult.getBy(__goal.getGoalKpi())) {
						bestResult = result;
					}

				} else if (Arrays.asList(Results.KPI.MAX_DRAWDOWN).contains(__goal.getGoalKpi())) {

					if (bestResult == null
					        || result.getBy(__goal.getGoalKpi()) < bestResult.getBy(__goal.getGoalKpi())) {
						bestResult = result;
					}

				} else {
					assert false;
				}
			}

			// Run with the next set of parameters.
			parameters = job.getNextParameters();
		}
		pushJobResults(job.getId(), jobResults, bestResult);
	}

	/**
	 * 
	 * @return
	 */
	public abstract Results runStrategy(BaseBarFeed feed, Parameters parameters);

	@Override
	public void run() {

		try {
			getLogger().info("Started running");
			// Get the instruments and bars.
			Pair<List<String>, List<Bars>> insBarsPair = getInstrumentsAndBars();
			Bar.Frequency barsFreq = getBarsFrequency();

			// Process jobs
			Job job = getNextJob();
			while (job != null) {
				__processJob(job, barsFreq, insBarsPair.getLeft(), insBarsPair.getRight());
				job = getNextJob();
			}

			getLogger().info("Finished running");

		} catch (Exception e) {
			getLogger().error("Finished running with errors: {} (exception stack trace: {})", e,
			        ExceptionUtils.getStackTrace(e));
		}
	}
}
